{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Building a Recommender System with Amazon SageMaker Factorization Machines\n",
    "\n",
    "---\n",
    "\n",
    "---\n",
    "\n",
    "## Background\n",
    "\n",
    "- Recommender systems were a catalyst for ML's popularity (Amazon, Netflix Prize)\n",
    "- User item matrix factorization is a core methodology\n",
    "- Factorization machines combine linear prediction with a factorized representation of pairwise feature interaction\n",
    "\n",
    "$$\\hat{r} = w_0 + \\sum_{i} {w_i x_i} + \\sum_{i} {\\sum_{j > i} {\\langle v_i, v_j \\rangle x_i x_j}}$$\n",
    "\n",
    "- SageMaker has a highly scalable factorization machines algorithm built-in\n",
    "- To learn more about the math behind _factorization machines_, [this paper](https://www.csie.ntu.edu.tw/~b97053/paper/Rendle2010FM.pdf) is a great resource"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sagemaker\n",
    "\n",
    "sagemaker_session = sagemaker.Session()\n",
    "role = sagemaker.get_execution_role()\n",
    "bucket = sagemaker_session.default_bucket()\n",
    "region_name = boto3.Session().region_name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "base = 'recommender'\n",
    "prefix = 'sagemaker' + base"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sagemaker\n",
    "import os\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import boto3\n",
    "import json\n",
    "import io\n",
    "import matplotlib.pyplot as plt\n",
    "import sagemaker.amazon.common as smac\n",
    "from sagemaker.predictor import json_deserializer\n",
    "from scipy.sparse import csr_matrix"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Download Dataset\n",
    "\n",
    "[Amazon Reviews AWS Public Dataset](https://s3.amazonaws.com/amazon-reviews-pds/readme.html)\n",
    "- 1 to 5 star ratings\n",
    "- 2M+ Amazon customers\n",
    "- 160K+ digital videos "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dataset columns:\n",
    "\n",
    "- `marketplace`: 2-letter country code (in this case all \"US\").\n",
    "- `customer_id`: Random identifier that can be used to aggregate reviews written by a single author.\n",
    "- `review_id`: A unique ID for the review.\n",
    "- `product_id`: The Amazon Standard Identification Number (ASIN).  `http://www.amazon.com/dp/<ASIN>` links to the product's detail page.\n",
    "- `product_parent`: The parent of that ASIN.  Multiple ASINs (color or format variations of the same product) can roll up into a single parent.\n",
    "- `product_title`: Title description of the product.\n",
    "- `product_category`: Broad product category that can be used to group reviews (in this case digital videos).\n",
    "- `star_rating`: The review's rating (1 to 5 stars).\n",
    "- `helpful_votes`: Number of helpful votes for the review.\n",
    "- `total_votes`: Number of total votes the review received.\n",
    "- `vine`: Was the review written as part of the [Vine](https://www.amazon.com/gp/vine/help) program?\n",
    "- `verified_purchase`: Was the review from a verified purchase?\n",
    "- `review_headline`: The title of the review itself.\n",
    "- `review_body`: The text of the review.\n",
    "- `review_date`: The date the review was written."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!mkdir -p /tmp/recsys/\n",
    "!aws s3 cp s3://amazon-reviews-pds/tsv/amazon_reviews_us_Digital_Video_Download_v1_00.tsv.gz /tmp/recsys/"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "df = pd.read_csv('/tmp/recsys/amazon_reviews_us_Digital_Video_Download_v1_00.tsv.gz', delimiter='\\t',error_bad_lines=False)\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Drop some fields that won't be used"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = df[['customer_id', 'product_id', 'product_title', 'star_rating', 'review_date']]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Most users don't rate most movies - Check our long tail"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "customers = df['customer_id'].value_counts()\n",
    "products = df['product_id'].value_counts()\n",
    "\n",
    "quantiles = [0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.96, 0.97, 0.98, 0.99, 1]\n",
    "print('customers\\n', customers.quantile(quantiles))\n",
    "print('products\\n', products.quantile(quantiles))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Filter out customers who haven't rated many movies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "customers = customers[customers >= 5]\n",
    "products = products[products >= 10]\n",
    "\n",
    "reduced_df = df.merge(pd.DataFrame({'customer_id': customers.index})).merge(pd.DataFrame({'product_id': products.index}))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Create a sequential index for customers and movies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "customers = reduced_df['customer_id'].value_counts()\n",
    "products = reduced_df['product_id'].value_counts()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "customer_index = pd.DataFrame({'customer_id': customers.index, 'user': np.arange(customers.shape[0])})\n",
    "product_index = pd.DataFrame({'product_id': products.index, \n",
    "                              'item': np.arange(products.shape[0]) + customer_index.shape[0]})\n",
    "\n",
    "reduced_df = reduced_df.merge(customer_index).merge(product_index)\n",
    "reduced_df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Count days since first review (included as a feature to capture trend)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "reduced_df['review_date'] = pd.to_datetime(reduced_df['review_date'])\n",
    "customer_first_date = reduced_df.groupby('customer_id')['review_date'].min().reset_index()\n",
    "customer_first_date.columns = ['customer_id', 'first_review_date']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "reduced_df = reduced_df.merge(customer_first_date)\n",
    "reduced_df['days_since_first'] = (reduced_df['review_date'] - reduced_df['first_review_date']).dt.days\n",
    "reduced_df['days_since_first'] = reduced_df['days_since_first'].fillna(0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Split into train and test datasets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_df = reduced_df.groupby('customer_id').last().reset_index()\n",
    "\n",
    "train_df = reduced_df.merge(test_df[['customer_id', 'product_id']], \n",
    "                            on=['customer_id', 'product_id'], \n",
    "                            how='outer', \n",
    "                            indicator=True)\n",
    "train_df = train_df[(train_df['_merge'] == 'left_only')]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Create Sparse Matrices"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Factorization machines expects data to look something like:\n",
    "  - Sparse matrix\n",
    "  - Target variable is that user's rating for a movie\n",
    "  - One-hot encoding for users ($N$ features)\n",
    "  - One-hot encoding for movies ($M$ features)\n",
    "\n",
    "|Rating|User1|User2|...|UserN|Movie1|Movie2|Movie3|...|MovieM|Feature1|Feature2|...|\n",
    "|---|---|---|---|---|---|---|---|---|---|---|---|---|\n",
    "|4|1|0|...|0|1|0|0|...|0|20|2.2|...|\n",
    "|5|1|0|...|0|0|1|0|...|0|17|9.1|...|\n",
    "|3|0|1|...|0|1|0|0|...|0|3|11.0|...|\n",
    "|4|0|1|...|0|0|0|1|...|0|15|6.4|...|\n",
    "\n",
    "\n",
    "- Wouldn't want to hold this full matrix in memory\n",
    "  - Create a sparse matrix\n",
    "  - Designed to work efficiently with CPUs. Some parts of training for more dense matrices can be parallelized with GPUs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def to_csr_matrix(df, num_users, num_items):\n",
    "    feature_dim = num_users + num_items + 1\n",
    "    data = np.concatenate([np.array([1] * df.shape[0]),\n",
    "                           np.array([1] * df.shape[0]),\n",
    "                           df['days_since_first'].values])\n",
    "    row = np.concatenate([np.arange(df.shape[0])] * 3)\n",
    "    col = np.concatenate([df['user'].values,\n",
    "                          df['item'].values,\n",
    "                          np.array([feature_dim - 1] * df.shape[0])])\n",
    "    return csr_matrix((data, (row, col)), \n",
    "                      shape=(df.shape[0], feature_dim), \n",
    "                      dtype=np.float32)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def to_s3_protobuf(csr, label, bucket, prefix, channel, splits):\n",
    "    indices = np.array_split(np.arange(csr.shape[0]), splits)\n",
    "    for i in range(len(indices)):\n",
    "        index = indices[i]\n",
    "        buf = io.BytesIO()\n",
    "        smac.write_spmatrix_to_sparse_tensor(buf, csr[index, ], label[index])\n",
    "        buf.seek(0)\n",
    "        boto3.client('s3').upload_fileobj(buf, bucket, '{}/{}/data-{}'.format(prefix, channel, i))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Convert to sparse recordIO-wrapped protobuf that SageMaker factorization machines expects"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train Dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_csr = to_csr_matrix(train_df, customer_index.shape[0], product_index.shape[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "to_s3_protobuf(train_csr, train_df['star_rating'].values.astype(np.float32), bucket, prefix, channel='train', splits=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Test Dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_csr = to_csr_matrix(test_df, customer_index.shape[0], product_index.shape[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "to_s3_protobuf(test_csr, test_df['star_rating'].values.astype(np.float32), bucket, prefix, channel='test', splits=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Train the Model\n",
    "\n",
    "- Create a [SageMaker Python SDK](https://github.com/aws/sagemaker-python-sdk) estimator to run a training jobs and specify:\n",
    "  - Algorithm container image\n",
    "  - IAM role\n",
    "  - Hardware setup\n",
    "  - S3 output location\n",
    "  - Algorithm hyperparameters\n",
    "    - `feature_dim`: $N + M + 1$ (additional feature is `days_since_first` to capture trend)\n",
    "    - `num_factors`: number of factor dimensions (increasing too much can lead to overfitting)\n",
    "    - `epochs`: number of full passes through the dataset\n",
    "- `.fit()` points to training and test data in S3 and begins the training job\n",
    "\n",
    "**Note**: For AWS accounts registered in conjunction with a workshop, default instance limits may prevent the use of `ml.c5.2xlarge` (and other equally powerful instances), and may require a lower value for `train_instance_count` depending on the instance type chosen. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# # https://github.com/aws/sagemaker-python-sdk/issues/1985\n",
    "# # container = sagemaker.image_uris.retrieve(region_name, \"blazingtext\", \"latest\")\n",
    "\n",
    "# image_uri = ''\n",
    "\n",
    "# if region_name == 'us-west-1':\n",
    "#     image_uri = '632365934929.dkr.ecr.us-west-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'us-west-2':\n",
    "#     image_uri = '433757028032.dkr.ecr.us-west-2.amazonaws.com'\n",
    "    \n",
    "# if region_name =='us-east-1':\n",
    "#     image_uri = '811284229777.dkr.ecr.us-east-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'us-east-2':\n",
    "#     image_uri = '825641698319.dkr.ecr.us-east-2.amazonaws.com'\n",
    "\n",
    "# if region_name =='ap-east-1':\n",
    "#     image_uri = '286214385809.dkr.ecr.ap-east-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'ap-northeast-1':\n",
    "#     image_uri = '501404015308.dkr.ecr.ap-northeast-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'ap-northeast-2':\n",
    "#     image_uri = '306986355934.dkr.ecr.ap-northeast-2.amazonaws.com'\n",
    "\n",
    "# if region_name == 'ap-south-1':\n",
    "#     image_uri = '991648021394.dkr.ecr.ap-south-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'ap-southeast-1':\n",
    "#     image_uri = '475088953585.dkr.ecr.ap-southeast-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'ap-southeast-2':\n",
    "#     image_uri = '544295431143.dkr.ecr.ap-southeast-2.amazonaws.com'\n",
    "\n",
    "# if region_name == 'ca-central-1':\n",
    "#     image_uri = '469771592824.dkr.ecr.ca-central-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'cn-north-1':\n",
    "#     image_uri = '390948362332.dkr.ecr.cn-north-1.amazonaws.com.cn'\n",
    "\n",
    "# if region_name == 'cn-northwest-1':\n",
    "#     image_uri = '387376663083.dkr.ecr.cn-northwest-1.amazonaws.com.cn'\n",
    "\n",
    "# if region_name == 'eu-central-1': \n",
    "#     image_uri = '813361260812.dkr.ecr.eu-central-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'eu-north-1':\n",
    "#     image_uri = '669576153137.dkr.ecr.eu-north-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'eu-west-1':\n",
    "#     image_uri = '685385470294.dkr.ecr.eu-west-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'eu-west-2':\n",
    "#     image_uri = '644912444149.dkr.ecr.eu-west-2.amazonaws.com'\n",
    "\n",
    "# if region_name == 'eu-west-3':\n",
    "#     image_uri = '749696950732.dkr.ecr.eu-west-3.amazonaws.com'\n",
    "\n",
    "# if region_name == 'me-south-1':\n",
    "#     image_uri = '249704162688.dkr.ecr.me-south-1.amazonaws.com'\n",
    "    \n",
    "# if region_name == 'sa-east-1':\n",
    "#     image_uri = '855470959533.dkr.ecr.sa-east-1.amazonaws.com'\n",
    "\n",
    "# if region_name == 'us-gov-west-1':\n",
    "#     image_uri = '226302683700.dkr.ecr.us-gov-west-1.amazonaws.com'\n",
    "    \n",
    "# # https://docs.aws.amazon.com/en_us/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html\n",
    "# image_uri = '{}/factorization-machines:1'.format(image_uri)\n",
    "\n",
    "# print('Using SageMaker container: {} ({})'.format(image_uri, region_name))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "image_uri = '382416733822.dkr.ecr.us-east-1.amazonaws.com/factorization-machines:1'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# from sagemaker.amazon.amazon_estimator import get_image_uri\n",
    "\n",
    "# image_uri = get_image_uri(region_name, \"factorization-machines\")\n",
    "# print(image_uri)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "fm = sagemaker.estimator.Estimator(\n",
    "    image_uri=image_uri,\n",
    "    role=role, \n",
    "    train_instance_count=1,\n",
    "    train_instance_type='ml.c5.xlarge',\n",
    "    output_path='s3://{}/{}/output'.format(bucket, prefix),\n",
    "    base_job_name=base,\n",
    "    sagemaker_session=sagemaker_session)\n",
    "\n",
    "fm.set_hyperparameters(\n",
    "    feature_dim=customer_index.shape[0] + product_index.shape[0] + 1,\n",
    "    predictor_type='regressor',\n",
    "    mini_batch_size=1000,\n",
    "    num_factors=256,\n",
    "    epochs=3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sagemaker.inputs import TrainingInput\n",
    "\n",
    "train_input = TrainingInput(s3_data='s3://{}/{}/train/'.format(bucket, prefix), \n",
    "                            distribution='ShardedByS3Key')\n",
    "\n",
    "test_input = TrainingInput(s3_data='s3://{}/{}/test/'.format(bucket, prefix), \n",
    "                            distribution='ShardedByS3Key')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fm.fit({'train': train_input, \n",
    "        'test': test_input})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Host the Endpoint\n",
    "\n",
    "Deploy trained model to a real-time production endpoint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fm_serializer(df):\n",
    "    feature_dim = customer_index.shape[0] + product_index.shape[0] + 1\n",
    "    js = {'instances': []}\n",
    "    for index, data in df.iterrows():\n",
    "        js['instances'].append({'data': {'features': {'values': [1, 1, data['days_since_first']],\n",
    "                                                      'keys': [data['user'], data['item'], feature_dim - 1],\n",
    "                                                      'shape': [feature_dim]}}})\n",
    "    return json.dumps(js)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "fm_predictor = fm.deploy(instance_type='ml.m4.xlarge', \n",
    "                         initial_instance_count=1,\n",
    "                         serializer=fm_serializer,\n",
    "                         deserializer=sagemaker.deserializers.JSONDeserializer())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Setup the predictor request handler\n",
    "Serialize the request data to match what the model is expecting"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#fm_predictor.content_type = 'application/json'\n",
    "#fm_predictor.serializer = fm_serializer\n",
    "#fm_predictor.deserializer = json_deserializer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Show some test data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_df.head(25)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Pick a single customer from the dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_customer = test_df.iloc[[20]]\n",
    "test_df.iloc[[20]] # peek at the data to confirm it's the one we wanted"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Pass `test_customer` to predictor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "fm_predictor.predict(test_customer)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Make a dataframe for an arbitrary customer and movie pair and test it out!\n",
    "\n",
    "Our `fm_serializer` requires 3 inputs to perform a prediction:\n",
    " - `user` id for a customer (type = num)\n",
    " - `item` id for a movie (type = num)\n",
    " - `days_since_first` review (type = double)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "fake_customer = test_customer # make a copy of the test_customer we pulled out before to modify\n",
    "desired_user_id = 65884 # person who rated Dexter with 5 stars\n",
    "desired_item_id = 140461 # Code for True Blood: Season 1\n",
    "desired_review_days = 28.0 # arbitrary number of days since first review\n",
    "\n",
    "#fake_customer_data = {'user' : desired_user_id, 'item' : desired_item_id, 'days_since_first' : desired_review_days}\n",
    "#fake_customer = pd.DataFrame(fake_customer_data, index=[0])\n",
    "fake_customer['user'] = desired_user_id\n",
    "fake_customer['item'] = desired_item_id\n",
    "fake_customer['days_since_first'] = desired_review_days\n",
    "\n",
    "# print the details for this fake customer\n",
    "fake_customer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fm_predictor.predict(fake_customer)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Clean-up the endpoint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# fm_predictor.delete_endpoint()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Release Resources"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%javascript\n",
    "Jupyter.notebook.save_checkpoint();\n",
    "Jupyter.notebook.session.delete();"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "conda_python3",
   "language": "python",
   "name": "conda_python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
